#ifndef _TYPECHECK_H
#define _TYPECHECK_H

#include <iostream>
#include <string>
#include <vector>
#include "semanticException.h"

using namespace std;

class Symbol
{
    string name;
    string type;
    vector<Symbol>* parameters = nullptr;

public:

    Symbol() = default;

    Symbol (const Symbol &that)
    {
        name = that.name;
        type = that.type;
        parameters = that.parameters;
    }

    explicit Symbol(string &n) : name(n), type("void"){}

    Symbol(string n, string t) : name(move(n)), type(move(t))
    {
        if(type == "procedure")
        {
            parameters = new vector<Symbol>();
        }
    }

    /**
     * Get a name of identifier.
     *
     * @return name of identifier
     */
    string getName()
    {
        return name;
    }

    /**
     * Get a type of identifier.
     *
     * @return type of identifier
     */
    string & getType()
    {
        return type;
    }

    /**
     * Add a type to the identifier.
     *
     * @param type will be add to identifier
     */
    void setType(const string &type)
    {
        this->type = type;
    }

    /**
 * Take the amount of parameters.
 *
 * @return return amount of parameters.
 */
    int getParametersSize()
    {
        return static_cast<int>(parameters->size());
    }

/**
 * Get parameter name located at i position.
 *
 * @param i position
 * @return return a name parameter.
 */
    Symbol getParamater(unsigned int i)
    {
        return parameters->at(i);
    }

/**
 * Add a parameter to this procedure.
 *
 * @param parameter Symbol
 */
    void addParameter(Symbol &parameter)
    {
        parameters->push_back(parameter);
    }
};

class TypeControl
{
    string MARK = "$";
    vector<string> stackExpression;
    vector<string> operationStack;
    vector<string> procedureParametersStack;

    bool callProcedure = false;
    Symbol* procedureSymbol = nullptr;

public:

    TypeControl() = default;

    TypeControl(TypeControl &that)
    {
        stackExpression = that.stackExpression;
        operationStack = that.operationStack;
        procedureParametersStack = that.procedureParametersStack;

    }

    void pushMark()
    {
        stackExpression.push_back(MARK);
    }

    void popMark() throw()
    {
            auto i = stackExpression.size()-1;
            string &result = stackExpression.at(i);

            stackExpression.erase(stackExpression.begin() + i);
            i--;
            stackExpression.erase(stackExpression.begin() + i);

            pushType(result);

    }

    void pushType(string &type) throw()
    {
            stackExpression.push_back(type);

            auto i = stackExpression.size()-1;
            if(i < 1) return;

            int count = 0;
            while (stackExpression.at(i) != (MARK)) {
                count++;
                i--;
            }


            auto x1 = stackExpression.size()-1;
            auto x2 = x1-1;

            if(count == 2) makeOperation(x1, x2);
    }

    void popType()
    {
        stackExpression.erase(stackExpression.end());
    }

    void refreshStack(string typeResult) throw()
    {
            popType();
            popType();
            pushType(typeResult);
    }

    void makeOperation(unsigned int x1, unsigned int x2) throw()
    {
            if(stackExpression.at(x1) == ("integer") && stackExpression.at(x2) == ("integer")) {
                if(getLastOperation() != ("relational")) {
                    refreshStack("integer");
                } else {
                    refreshStack("boolean");
                }

                popOperation();
            }
            else if(stackExpression.at(x1) == ("integer") && stackExpression.at(x2) == ("real")) {
                if(getLastOperation() != ("relational")) {
                    refreshStack("real");
                } else {
                    refreshStack("boolean");
                }
                popOperation();
            }
            else if(stackExpression.at(x1) == ("real") && stackExpression.at(x2) == ("integer")) {
                if(getLastOperation() != ("relational")) {
                    refreshStack("real");
                } else {
                    refreshStack("boolean");
                }
                popOperation();
            }
            else if(stackExpression.at(x1) == ("real") && stackExpression.at(2) == ("real")) {
                if(getLastOperation() != ("relational")) {
                    refreshStack("real");
                } else {
                    refreshStack("boolean");
                }
                popOperation();
            }
            else if(stackExpression.at(x1) == ("boolean") && stackExpression.at(x2) == ("boolean")) {
                if(getLastOperation() == ("relational")) {
                    refreshStack("boolean");
                }
                else {
                    string par = "Error in operation";
                    throw SemanticException(par);
                }
                popOperation();
            }
            else {
                popOperation();
                string par = "Error in types combination!";
                throw SemanticException(par);
            }
    }

    void verifyResult(const string &typeVar) throw()
    {

            if(stackExpression.at(0) == ("integer") && typeVar == ("integer")) {
                stackExpression.clear();
            }
            else if(stackExpression.at(0) == ("integer") && typeVar == ("real")) {
                stackExpression.clear();
            }
            else if(stackExpression.at(0) == ("real") && typeVar == ("real")) {
                stackExpression.clear();
            }
            else if(stackExpression.at(0) == ("boolean") && typeVar == ("boolean")) {
                stackExpression.clear();
            }
            else {

                stackExpression.clear();
                string par = "Error in assigning value to a variable!";
                throw SemanticException(par);
            }
    }

    void reset()
    {
        stackExpression.clear();
    }

    void pushOperation(const string &operation) {
        operationStack.push_back(operation);
    }

    void popOperation() {
        operationStack.erase(operationStack.end());
    }

    string getLastOperation() {
        return operationStack.at(operationStack.size()-1);
    }

    void setCallProcedure(bool b, Symbol &procedureSymbol) {
        callProcedure = b;
        this->procedureSymbol = &procedureSymbol;
    }

    bool isCallProcedure() {
        return callProcedure;
    }

    void pushParameter(const string &type)
    {
        procedureParametersStack.push_back(type);
    }

    string getFirstType()
    {
        return stackExpression.at(0);
    }

    void resetProcedureControl()
    {
        procedureParametersStack.clear();
        stackExpression.clear();
        callProcedure = false;
        this->procedureSymbol = nullptr ;
    }

    void verifyResultProcedureCall() throw()
    {
            if(procedureSymbol->getParametersSize() !=  procedureParametersStack.size())
            {

                string par = "Parameters number error!";
                throw SemanticException(par);

            } else
            {
                for (unsigned int i = 0; i < procedureParametersStack.size(); i++) {
                    if (procedureParametersStack.at(i) == ("integer") && procedureSymbol->getParamater(i).getType() == ("integer")) {

                    } else if (procedureParametersStack.at(i) == ("integer") && procedureSymbol->getParamater(i).getType() == ("real")) {

                    } else if (procedureParametersStack.at(i) == ("real") && procedureSymbol->getParamater(i).getType() == ("real")) {

                    }else if (procedureParametersStack.at(i) == ("boolean") && procedureSymbol->getParamater(i).getType() == ("boolean")) {

                    } else {
                        string par = "Error in assigning value to parameter";
                        throw SemanticException(par);
                    }
                }
            }
            resetProcedureControl();
    }

};

#endif